Flutter UI渲染分析

Flutter UI渲染分析

1、前言

本篇文章主要介绍Flutter 渲染框架及其渲染过程

Flutter是谷歌的移动UI框架,在此之前也有类似ReactNative、Weex等跨端方案,Flutter在一定程度上借鉴了ReactNative的思想,采用三棵树 其中element tree diff管理,来触发renderTree的刷新,并且不同于android这种命令式视图开发,采用了声明式,下面将一一介绍。

2、编程范式的改变

在Android视图开发中是命令式的,view大多数都是在xml声明,开发者然后通过id找出view,数据更新时,仍需要开发者关注需要变化的view,再调用方法比如 setText之类的使其发生改变;
但是在Flutter中视图的开发是声明式的,开发者需要维护好一套数据集合以及绑定好widgetTree,这样后面数据变化时候widget会根据数据来渲染,开发者就不再关注每个组件,关心核心数据即可。

3、Flutter 渲染框架介绍

Flutter的渲染框架分为Framework和Engine两层,应用是基于Framework层开发,其中

  • Framework层负责渲染中的Build、Layout、Paint、生成Layer等环节,使用Dart语言
  • Engine层是C++实现的渲染引擎,负责把Framework生成的Layer组合,生成纹理,然后通过OpenGL接口向GPU提交渲染数据

该跨平台应用框架没有使用webview或者平台自带的组件,使用自身的高性能渲染引擎Skia 自绘,组件之间可以任意组合
image.png

4、视图树

flutter中通过各种各样的widget组合使用,视图树中包含了以下三种树 Widget、Element、RenderObject,对应关系如下

image.png

  • Widget:存放渲染内容、视图布局信息,widget的属性最好都是immutable
  • Element:存放上下文,通过Element遍历视图树,Element同时持有Widget和RenderObject(BuilderOwner)
  • RenderObject:根据Widget的布局属性进行layout,paint Widget传人的内容(PipeLineOwner)

通常 我们创建widget树,然后调用runApp(rootWidget),将rootWidget传给rootElement,作为rootElement的子节点,生成Element树,由Element树生成Render树
image.png

widget是immutable,数据变化会重绘,如何避免资源消耗

Flutter界面开发是一种响应式的编程,当数据发生变化时通知到可变更的节点(statefullWidget或者rootwidget),但是每次数据变更,都会触发widgetTree的重绘,由于widget只是持有一些渲染的配置信息而已,不是真正触发渲染的对象,非常轻量级,flutter团队对widget的创建、销毁做了优化,不用担心整个widget树重新创建带来的性能问题。RenderObject才是真正渲染时使用,涉及到layout、paint等复杂操作,是一个真正渲染的view,二者被Element Tree持有,ElementTree通过Diff 算法来将不断变化的widget转变为相对稳定的RenderObject。
当我们不断改变widget时,BuilderOwner收到widgetTree会与之前的widgetTree作对比,在ElementTree上只更新变化的部分,当Elment变化之后 与之对应的RenderObject也就更新了,如下图所示
image.png可以看到WidgetTree全部被替换了,但是ElmentTree和RenderObjectTree只替换了变化的部分image.png
其中 PipelineOwner类似于Android中的ViewRootImpl,管理着真正需要绘制的View,
最后PipelineOwner会对RenderObjectTree中发生变化节点的进行layout、paint、合成等等操作,最后交给底层引擎渲染。

Widget、Element、RenderObject之间的关系

在介绍Elment Tree的Diff规则之前,先介绍下,这三者之前的关系,之前也大致提到 Elment Tree持有了Element同时持有Widget和RenderObject(BuilderOwner),我们先从代码入手

image.png

可以看出 Widget抽象类有3个关键能力

  • 保证自身唯一性的key
  • 创建Element的create
  • canUpdate

从上面类图也可以看出,Element和RenderObject都是由Widget创建出来,也并不是每一个Widget都有与之对应的RenderObject

Widget、Element、RenderObject 的第一次创建与关联


在Android中ViewTree

1
2
3
4
-PhoneWindow
- DecorView
- TitleView
- ContentView

而在Flutter中则比较简单,只有底层的root widget

1
2
3
- RenderObjectToWidgetAdapter<RenderBox>
- MyApp (自定义)
- MyMaterialApp (自定义)

其中RenderObjectToWidgetAdapter 也是一个renderObjectWidget,通过注释可以发现它是runApp启动时“A bridge from a [RenderObject] to an [Element] tree.”
runApp代码

1
2
3
4
5
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)
..scheduleWarmUpFrame();
}

WidgetsFlutterBinding 初始化了一系列的Binding,这些Binding持有了我们上面说的一些owner,比如BuildOwner,PipelineOwner,所以随着WidgetsFlutterBinding的初始化,其他的Binding也被初始化了,

GestureBinding 提供了 window.onPointerDataPacket 回调,绑定 Framework 手势子系统,是 Framework 事件模型与底层事件的绑定入口
ServicesBinding 提供了 window.onPlatformMessage 回调, 用于绑定平台消息通道(message channel),主要处理原生和 Flutter 通信
SchedulerBinding 提供了 window.onBeginFrame 和 window.onDrawFrame 回调,监听刷新事件,绑定 Framework 绘制调度子系统
PaintingBinding 绑定绘制库,主要用于处理图片缓存
SemanticsBinding 语义化层与 Flutter engine 的桥梁,主要是辅助功能的底层支持
RendererBinding 提供了 window.onMetricsChanged 、window.onTextScaleFactorChanged 等回调。它是渲染树与 Flutter engine 的桥梁
WidgetsBinding 提供了 window.onLocaleChanged、onBuildScheduled 等回调。它是 Flutter widget 层与 engine 的桥梁

继续跟进下attachRootWidget(app)

1
2
3
4
5
6
7
void attachRootWidget(Widget rootWidget) {
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget
).attachToRenderTree(buildOwner, renderViewElement);
}

内部创建了 RenderObjectToWidgetAdapter 并将我们传入的app 自定义widget做了child,接着执行attachToRenderTree这个方法,创建了第一个Element和RenderObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
if (element == null) {
owner.lockState(() {
element = createElement(); //创建rootElement
element.assignOwner(owner); //绑定BuildOwner
});
owner.buildScope(element, () { //子widget的初始化从这里开始
element.mount(null, null); // 初始化子Widget前,先执行rootElement的mount方法
});
} else {
...
}
return element;
}

image.png

我们解释一下上面的图片,Root的创建比较简单:

  • 1.attachRootWidget(app) 方法创建了Root[Widget](也就是 RenderObjectToWidgetAdapter)
  • 2.紧接着调用attachToRenderTree方法创建了 Root[Element]
  • 3.Root[Element]尝试调用mount方法将自己挂载到父Element上,因为自己就是root了,所以没有父Element,挂空了
  • 4.mount的过程中会调用Widget的createRenderObject,创建了 Root[RenderObject]

它的child,也就是我们传入的app是怎么挂载父控件上的呢?

  • 5.我们将app作为Root[Widget](也就是 RenderObjectToWidgetAdapter),appp[Widget]也就成了为root[Widget]的child[Widget]
  • 6.调用owner.buildScope,开始执行子Tree的创建以及挂载,敲黑板!!!这中间的流程和WidgetTree的刷新流程是一模一样的,详细流程我们后面讲!
  • 7.调用createElement方法创建出Child[Element]
  • 8.调用Element的mount方法,将自己挂载到Root[Element]上,形成一棵树
  • 9.挂载的同时,调用widget.createRenderObject,创建Child[RenderObject]
  • 10.创建完成后,调用attachRenderObject,完成和Root[RenderObject]的链接

就这样,WidgetTree、ElementTree、RenderObject创建完成,并有各自的链接关系。


这里有两个操作需要注意下,

mount

1
2
3
4
5
6
7
8
9
10
11
abstract class Element

void mount(Element parent, dynamic newSlot) {
_parent = parent; //持有父Element的引用
_slot = newSlot;
_depth = _parent != null ? _parent.depth + 1 : 1;//当前节点的深度
_active = true;
if (parent != null) // Only assign ownership if the parent is non-null
_owner = parent.owner; //每个Element的buildOwner,都来自父类的BuildOwner
...
}

我们先看一下Element的挂载,就是让_parent持有父Element的引用,因为RootElement 是没有父Element的,所以参数传了null:element.mount(null, null);
还有两个值得注意的地方:

  • 节点的深度_depth 也是在这个时候计算的,深度对刷新很重要
  • 每个Element的buildOwner,都来自父类的BuildOwner,这样可以保证一个ElementTree,只由一个BuildOwner来维护。

    RenderObjectElement

1
2
3
4
5
6
7
8
9
abstract class RenderObjectElement:

@override
void attachRenderObject(dynamic newSlot) {
...
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
_ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);
...
}

RenderObject与父RenderObject的挂载稍微复杂了点。通过代码我们可以看到需要先查询一下自己的AncestorRenderObject,这是为什么呢?
还记得之前我们讲过,每一个Widget都有一个对应的Element,但Element不一定会有对应的RenderObject。所以你的父Element并不一有RenderObject,这个时候就需要向上查找。

1
2
3
4
5
6
RenderObjectElement _findAncestorRenderObjectElement() {
Element ancestor = _parent;
while (ancestor != null && ancestor is! RenderObjectElement)
ancestor = ancestor._parent;
return ancestor;
}

通过代码我们也可以看到,find方法在向上遍历Element,直到找到RenderObjectElement,RenderObjectElement肯定是有对应的RenderObject了,这个时候在进行RenderObject子父间的挂载。

5、渲染过程

当需要更新UI的时候,Framework通知Engine,Engine会等到下个Vsync信号到达的时候,会通知Framework,然后Framework会进行animations,
build,layout,compositing,paint,最后生成layer提交给Engine。Engine会把layer进行组合,生成纹理,最后通过Open Gl接口提交数据给GPU,
GPU经过处理后在显示器上面显示。整个流程如下图:

6、渲染触发 (setState)

setState背后发生了什么

在Flutter开发应用的时候,当需要更新的UI的时候,需要调用一下setState方法,然后就可以实现了UI的更新,我们接下来分析一下该方法做哪些事情。

1
2
3
4
void setState(VoidCallback fn) {
...
_element.markNeedsBuild(); //通过相应的element来实现更新,关于element,widget,renderOjbect这里不展开讨论
}

继续追踪

1
2
3
4
5
6
7
void markNeedsBuild() {
...
if (dirty)
return;
_dirty = true;
owner.scheduleBuildFor(this);
}

widget对应的element将自身标记为dirty状态,并调用owner.scheduleBuildFor(this);通知buildOwner进行处理

1
2
3
4
5
6
7
8
9
10
void scheduleBuildFor(Element element) {
...
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
onBuildScheduled(); //这是一个callback,调用的方法是下面的_handleBuildScheduled
}
_dirtyElements.add(element); //把当前element添加到_dirtyElements数组里面,后面重新build会遍历这个数组
element._inDirtyList = true;

}

后续MyStatefulWidget的build方法一定会被执行,执行后,会创建新的子Widget出来,原来的子Widget便被抛弃掉了,原来的子Widget肯定是没救了,但他们的Element大概率还是有救的,此时 buildOwner会将所有dirty的Element添加到_dirtyElements当中
经过Framework一连串的调用后,最终调用scheduleFrame来通知Engine需要更新UI,Engine就会在下个vSync到达的时候通过调用_drawFrame来通知Framework,然后Framework就会通过BuildOwner进行Build和PipelineOwner进行Layout,Paint,最后把生成Layer,组合成Scene提交给Engine。


底层引擎最终回到Dart层,并执行buildOwner的buildScope方法,首先从Engine回调Framework的入口开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void _drawFrame() { //Engine回调Framework入口 
_invoke(window.onDrawFrame, window._onDrawFrameZone);
}

//初始化的时候把onDrawFrame设置为_handleDrawFrame
void initInstances() {
super.initInstances();
_instance = this;
ui.window.onBeginFrame = _handleBeginFrame;
ui.window.onDrawFrame = _handleDrawFrame;
SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
}

void _handleDrawFrame() {
if (_ignoreNextEngineDrawFrame) {
_ignoreNextEngineDrawFrame = false;
return;
}
handleDrawFrame();
}
void handleDrawFrame() {
_schedulerPhase = SchedulerPhase.persistentCallbacks;//记录当前更新UI的状态
for (FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
}
}

void initInstances() {
....
addPersistentFrameCallback(_handlePersistentFrameCallback);
}

void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame();
}

void drawFrame() {
...
if (renderViewElement != null)
buildOwner.buildScope(renderViewElement); //先重新build widget
super.drawFrame();
buildOwner.finalizeTree();

}


核心方法 buildScope

1
2
3
void buildScope(Element context, [VoidCallback callback]){
...
}

需要传入一个Element的参数,这个方法通过字面意思应该理解就是对这个Element以下范围rebuild

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void buildScope(Element context, [VoidCallback callback]) {
...
try {
...
_dirtyElements.sort(Element._sort); //1.排序
...
int dirtyCount = _dirtyElements.length;
int index = 0;
while (index < dirtyCount) {
try {
_dirtyElements[index].rebuild(); //2.遍历rebuild
} catch (e, stack) {
}
index += 1;
}
} finally {
for (Element element in _dirtyElements) {
element._inDirtyList = false;
}
_dirtyElements.clear(); //3.清空
...
}
}

这里对上面方法做下解释

  • 第1步:按照Element的深度从小到大,对_dirtyElements进行排序

由于父Widget的build方法必然会触发子Widget的build,如果先build了子Widget,后面再build父Widget时,子Widget又要被build一次。所以这样排序之后,可以避免子Widget的重复build。

  • 第2步:遍历执行_dirtyElements当中element的rebuild方法

值得一提的是,遍历执行的过程中,也有可能会有新的element被加入到_dirtyElements集合中,此时会根据dirtyElements集合的长度判断是否有新的元素进来了,如果有,就重新排序。

element的rebuild方法最终会调用performRebuild(),而performRebuild()不同的Element有不同的实现

  • 第3步:遍历结束之后,清空dirtyElements集合

因此setState()过程主要工作是记录所有的脏元素,添加到BuildOwner对象的_dirtyElements成员变量,然后调用scheduleFrame来注册Vsync回调。 当下一次vsync信号的到来时会执行handleBeginFrame()和handleDrawFrame()来更新UI。

Element的Diff

在上面的第二步会遍历执行element的build方法
_dirtyElements[index].rebuild(); //2.遍历rebuild
element的rebuild方法最终会调用performRebuild(),而performRebuild()不同的Element有不同的实现,以下面两个为例

  • ComponentElement,是StatefulWidget和StatelessElement的父类
  • RenderObjectElement, 是有渲染功能的Element的父类
    ComponentElement的performRebuild()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void performRebuild() {
    Widget built;
    try {
    built = build();
    }
    ...
    try {
    _child = updateChild(_child, built, slot);
    }
    ...
    }

执行element的build();,以StatefulElement的build方法为例:Widget build() => state.build(this);。 就是执行了我们复写的StatefulWidget的state的build方法,此时创建出来的当然就是这个StatefulWidget的子Widget了

下面看下核心方法 Element updateChild(Element child, Widget newWidget, dynamic newSlot)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
...
//1
if (newWidget == null) {
if (child != null)
deactivateChild(child);
return null;
}

if (child != null) {
//2
if (child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
return child;
}
//3
if (Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
return child;
}
deactivateChild(child);
}
//4
return inflateWidget(newWidget, newSlot);
}

参数child 是上一次Element挂载的child Element, newWidget 是刚刚build出来的。updateChild有四种可能的情况

  • 1.如果刚build出来的widget等于null,说明这个控件被删除了,child Element可以被删除了。

  • 2.如果child的widget和新build出来的一样(Widget复用了),就看下位置一样不,不一样就更新下,一样就直接return了。Element还是旧的Element

  • 3.看下Widget是否可以update,Widget.canUpdate的逻辑是判断key值和运行时类型是否相等。如果满足条件的话,就更新,并返回。


中间商的差价哪来的呢?只要新build出来的Widget和上一次的类型和Key值相同,Element就会被复用!由此也就保证了虽然Widget在不停的新建,但只要不发生大的变化,那Element是相对稳定的,也就保证了RenderObject是稳定的!

  • 4.如果上述三个条件都没有满足的话,就调用 inflateWidget() 创建新的Element

这里再看下inflateWidget()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Element inflateWidget(Widget newWidget, dynamic newSlot) {
final Key key = newWidget.key;
if (key is GlobalKey) {
final Element newChild = _retakeInactiveElement(key, newWidget);
if (newChild != null) {
newChild._activateWithParent(this, newSlot);
final Element updatedChild = updateChild(newChild, newWidget, newSlot);
return updatedChild;
}
}
final Element newChild = newWidget.createElement();
newChild.mount(this, newSlot);
return newChild;
}

首先会尝试通过GlobalKey去查找可复用的Element,复用失败就调用Widget的方法创建新的Element,然后调用mount方法,将自己挂载到父Element上去,mount之前我们也讲过,会在这个方法里创建新的RenderObject。

RenderObjectElement的performRebuild()
1
2
3
4
5
@override
void performRebuild() {
widget.updateRenderObject(this, renderObject);
_dirty = false;
}

与ComponentElement的不同之处在于,没有去build,而是调用了updateRenderObject方法更新RenderObject。到这里我们基本就明白了Element是如何在中间应对Widget的多变,保障RenderObject的相对不变了

7、参考

0%